package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"runtime"
	"syscall"
	"time"

	log "github.com/sirupsen/logrus"
)

// log filename is {BaseFilename}.{PID}.log[.{Date}.{DayIndex}]
type TimedRotatingFileHook struct {
	pid           int
	logFolder     string
	baseFilename  string
	backupDays    int
	maxBytes      int
	writeBytes    int
	logDayIndex   int
	lastTimestamp time.Time
	logLevels     []log.Level
	fp            *os.File
}

func NewTimedRotatingFileHook(folder, filename string, backup, max_bytes int, levels []log.Level) *TimedRotatingFileHook {
	os.MkdirAll(folder, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR|syscall.S_IRGRP|syscall.S_IWGRP|syscall.S_IXGRP|syscall.S_IROTH|syscall.S_IXOTH)

	return &TimedRotatingFileHook{
		logDayIndex:   0,
		logLevels:     levels,
		writeBytes:    0,
		maxBytes:      max_bytes,
		backupDays:    backup,
		pid:           os.Getpid(),
		logFolder:     folder,
		baseFilename:  filename,
		lastTimestamp: time.Now(),
		fp:            nil,
	}
}

func (self *TimedRotatingFileHook) Levels() []log.Level {
	return self.logLevels
}

func (self *TimedRotatingFileHook) Fire(entry *log.Entry) error {
	line, err := entry.Bytes()
	if err != nil {
		return err
	}
	self.RolloverFile(entry.Time)
	count, err := self.fp.Write(line)
	self.writeBytes += count
	return err
}

func (self *TimedRotatingFileHook) RolloverFile(entry_time time.Time) {
	same_day := entry_time.Day() == self.lastTimestamp.Day()
	if self.fp != nil {
		if !same_day || self.writeBytes >= self.maxBytes {
			oldfilename := fmt.Sprintf("%s.%d", self.baseFilename, self.pid)
			newfilename := fmt.Sprintf("%s.%d.%s.%04x", self.baseFilename, self.pid, self.lastTimestamp.Format("2006-01-02"), self.logDayIndex)
			oldfilepath := path.Join(self.logFolder, oldfilename)
			newfilepath := path.Join(self.logFolder, newfilename)
			self.logDayIndex += 1
			self.writeBytes = 0
			self.fp.Close()
			self.fp = nil
			os.Rename(oldfilepath, newfilepath)
			if !same_day {
				self.lastTimestamp = entry_time
				self.ClearExpireLogFile()
				self.logDayIndex = 0
			}
		}
	}
	if self.fp == nil {
		filename := fmt.Sprintf("%s.%d", self.baseFilename, self.pid)
		filepath := path.Join(self.logFolder, filename)
		self.fp, _ = os.OpenFile(filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IRGRP|syscall.S_IWGRP|syscall.S_IROTH)
	}
}

func (self *TimedRotatingFileHook) ClearExpireLogFile() {
	if self.backupDays <= 0 {
		return
	}
	entries, err := os.ReadDir(self.logFolder)
	if err != nil {
		return
	}
	now := time.Now()
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			continue
		}
		if info.IsDir() {
			continue
		}
		if now.Sub(info.ModTime()) >= time.Duration(self.backupDays)*24*time.Hour {
			filepath := path.Join(self.logFolder, info.Name())
			os.Remove(filepath)
		}
	}
}

type SortingFields []string

func (s SortingFields) pos(a int) int {
	// time level file func ...fields... msg
	if s[a] == "msg" {
		return len(s)
	} else if s[a] == "time" {
		return 0
	} else if s[a] == "level" {
		return 1
	} else if s[a] == "file" {
		return 2
	} else if s[a] == "func" {
		return 3
	} else {
		return a
	}
}

func (s SortingFields) Len() int      { return len(s) }
func (s SortingFields) Swap(a, b int) { tmp := s[a]; s[a] = s[b]; s[b] = tmp }
func (s SortingFields) Less(a, b int) bool {
	return s.pos(a) < s.pos(b)
}

var binary string = "sample"

func init() {
	CallerPrettyfier := func(frame *runtime.Frame) (function string, file string) {
		function = frame.Function
		_, filename := path.Split(frame.File)
		file = fmt.Sprintf("%s:%d", filename, frame.Line)
		return
	}
	filename := fmt.Sprintf("%s.log", binary)
	log.AddHook(NewTimedRotatingFileHook("logs", filename, 30, 1*1024*1024, log.AllLevels))
	// log.SetFormatter(&log.JSONFormatter{
	// 	TimestampFormat:  "2006-01-02 15:04:05.999-07:00",
	// 	PrettyPrint:      false,
	// 	CallerPrettyfier: CallerPrettyfier,
	// })
	log.SetFormatter(&PlainTextFormatter{
		TimestampFormat:  "2006-01-02 15:04:05.000-07:00",
		CallerPrettyfier: CallerPrettyfier,
	})
	// log.SetFormatter(&log.TextFormatter{
	//      ForceColors:               false,
	//      DisableColors:             true,
	//      FullTimestamp:             true,
	//      QuoteEmptyFields:          true,
	//      EnvironmentOverrideColors: false,
	//      DisableSorting:            false,
	//      TimestampFormat:           "2006-01-02 15:04:05.999-07:00",
	//      SortingFunc: func(fields []string) {
	//              sort.Sort(SortingFields(fields))
	//      },
	//      CallerPrettyfier: CallerPrettyfier,
	// })
	log.SetOutput(os.Stdout)
	log.SetReportCaller(true)
	log.SetLevel(log.DebugLevel)
}

type PlainTextFormatter struct {
	TimestampFormat  string
	CallerPrettyfier func(*runtime.Frame) (function string, file string)
}

func (self *PlainTextFormatter) Format(entry *log.Entry) ([]byte, error) {
	buf := bytes.NewBufferString(entry.Time.Format(self.TimestampFormat))
	buf.WriteString(" ")
	buf.WriteString(entry.Level.String())
	buf.WriteString(" ")
	if entry.HasCaller() {
		funcVal := entry.Caller.Function
		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
		if self.CallerPrettyfier != nil {
			funcVal, fileVal = self.CallerPrettyfier(entry.Caller)
		}
		buf.WriteString(fileVal)
		buf.WriteString(" ")
		buf.WriteString(funcVal)
		buf.WriteString(" ")
	}
	fields, _ := json.Marshal(entry.Data)
	buf.Write(fields)
	buf.WriteString(" ")
	buf.WriteString(entry.Message)
	buf.WriteString("\n")
	return buf.Bytes(), nil
}

func Test() {
	log.WithFields(log.Fields{
		"animal": "walrus",
		"size":   10,
	}).Info("A group of walrus emerges from the ocean")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 122,
	}).Warn("The group's number increased tremendously!")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 100,
	}).Warn("The ice breaks!")

	// A common pattern is to re-use fields between logging statements by re-using
	// the logrus.Entry returned from WithFields()
	contextLogger := log.WithFields(log.Fields{
		"common": "this is a common field",
		"other":  "I also should be logged always",
	})

	for i := 0; i < 10; i++ {
		contextLogger.Debug("I'll be logged with common and other field")
		contextLogger.Trace("Me too")
	}
}
